// yadis-lib.js

// Library for Yadis; see http://yadis.org/
// Only the first two methods here should be of use, the rest
// are supporting functions.

// Does not execute optional preliminary HEAD step; uses browser cache
// when known to have been fetched before, change if this behavior is
// incorrect for your purposes.

// Constants for HTTP header accesses
var YADIS_HTTP_HEADER = "X-XRDS-Location";
var YADIS_MIME_TYPE   = "application/xrds+xml";
var XML_TEXT_MIME_TYPE  = "text/xml";
var XML_APP_MIME_TYPE   = "application/xml";
var HTTP_CONTENT_TYPE = "Content-Type";

function Yadis(yadisId) {
    this._id = yadisId;
};

/**
 * Call to find out if present page has Yadis-based OpenID
 * @param callback function(bool)
 *   @subparam bool true if URI can be used as a Yadis-based OpenID
 * @param doc DOM of present page
 */
Yadis.prototype.discover = function(callback, doc) {
    AppalachianDebug.trace("yadis-lib.js",'> Yadis.discover');
    var f = function(headers) {
        var answer = false;
        if (headers && headers.location != null && headers.location != "") {
            answer = true;
        } else {
            var ms = doc.getElementsByTagName('meta');
            for (var i = 0; i < ms.length; i++) {
                if (ms[i].hasAttribute("http-equiv") && ms[i].getAttribute("http-equiv").toLowerCase().indexOf(YADIS_HTTP_HEADER.toLowerCase()) != -1) {
                    answer = true;
                }
            }
        }
        callback(answer);
    };
    HeaderCache.get(f, this._id);
    AppalachianDebug.trace("yadis-lib.js",'< Yadis.discover');
};

/**
 * Obtain authorities for the Yadis-based OpenID
 * @param callback function(arr)
 *    @subparam arr An array of authorities
 * @param doc The DOM found at the Yadis-based OpenID URI
 */
Yadis.prototype.getAuthorities = function(callback, doc) {
    AppalachianDebug.trace("yadis-lib.js",'> Yadis.getAuthorities');
    var ylib = this;
    var f = function(headers) {
        if (headers && headers.location != null && headers.location != "") {
            ylib.processLocation(callback, headers.location);
        } else {
            ylib.processBody(callback, doc);
        }
    };
    HeaderCache.get(f, this._id);
    AppalachianDebug.trace("yadis-lib.js",'< Yadis.getAuthorities');
};

Yadis.prototype.processBody = function(callback, doc) {
    AppalachianDebug.trace("yadis-lib.js",'> Yadis.processBody');
    var loc;
    var metas = doc.wrappedJSObject.getElementsByTagName('meta');
    for (var i = 0; i < metas.length; i++) {
        if (metas[i].hasAttribute('http-equiv')) {
            if (metas[i].getAttribute('http-equiv').toLowerCase() == YADIS_HTTP_HEADER.toLowerCase()) {
                loc = metas[i].getAttribute('content');
                break;
            }
        }
    }
    AppalachianDebug.trace("yadis-lib.js",'< Yadis.processBody');
    this.processLocation(callback, loc);
};

Yadis.prototype.processLocation = function(callback, url) {
    AppalachianDebug.trace("yadis-lib.js",'> Yadis.processLocation ' + url);
    var ylib = this;
    var f = function(headers) {
        if (headers && headers[url] && headers[url].isXRDS) {
            var cont = function(data, channel) {
                callback(ylib.evaluateBody(data));
            };
            var err = function(channel) {
                // @@@ do something here?
            };
            var httplib = new YadisHTTP(url);
            httplib.doGET(cont, err, true, true);
            httplib = null;
        }
    };
    HeaderCache.get(f, url);
    AppalachianDebug.trace("yadis-lib.js",'< Yadis.processLocation');
};

Yadis.prototype.evaluateBody = function(xml) {
    AppalachianDebug.trace("yadis-lib.js",'> Yadis.evaluateBody');
    var parser = new DOMParser();
    var doc = parser.parseFromString(xml, "text/xml");
    var nsResolver = function(prefix) {
    var p = null;
    if (prefix == 'xrds')
        p = 'xri://$xrds';
    else if (prefix == 'x')
        p = 'xri://$xrd*($v*2.0)';
        return p;
    };
    var iterator = doc.evaluate("//x:Service[x:Type='http://openid.net/signon/1.0' or x:Type='http://openid.net/signon/1.1']/x:URI", doc, nsResolver, XPathResult.ANY_TYPE,null);
    var elmt = iterator.iterateNext();
    var i = 0;
    var authorities = [];
    while (elmt) {
        authorities[i++] = elmt.textContent;
        elmt = iterator.iterateNext();
    }
    AppalachianDebug.trace("yadis-lib.js",'< Yadis.evaluateBody');
    return authorities;
};

/**
 * Asynchronous HTTP GET
 */
function YadisHTTP(url) {
    this._url = url;
};

YadisHTTP.prototype.doGET = function(callback, error, cache, accept) {
    AppalachianDebug.trace("yadis-lib.js", "> YadisHTTP.doGET " + this._url);
    var ioService = Components.classes["@mozilla.org/network/io-service;1"].getService(Components.interfaces.nsIIOService);
    var uri = ioService.newURI(this._url, null, null);
    var channel = ioService.newChannelFromURI(uri);
    channel.QueryInterface(Components.interfaces.nsIRequest);
    channel.loadFlags |= cache ? Components.interfaces.nsIRequest.LOAD_FROM_CACHE : Components.interfaces.nsIRequest.LOAD_BYPASS_CACHE;
    channel.QueryInterface(Components.interfaces.nsIHttpChannel);
    if (accept) channel.setRequestHeader("Accept", YADIS_MIME_TYPE + "; q=1,text/html; q=0.9", false);
    var listener = new YadisStreamListener(channel, callback, error);
    channel.notificationCallbacks = listener;
    channel.asyncOpen(listener, null);
    AppalachianDebug.trace("yadis-lib.js", "< YadisHTTP.doGET");
};

function YadisStreamListener(channel, callback, error) {
    this._channel = channel;
    this._callback = callback;
    this._error = error;
}

YadisStreamListener.prototype = {
    _channel: "",
    _data: "",

    onStartRequest: function (req, context) {
        this._data = "";
    },

    onDataAvailable: function(req, context, stream, sourceOffset, length) {
        var scriptableInputStream = Components.classes["@mozilla.org/scriptableinputstream;1"].createInstance(Components.interfaces.nsIScriptableInputStream);
        scriptableInputStream.init(stream);

        this._data += scriptableInputStream.read(length);
    },

    onStopRequest: function(req, context, status) {
        if (Components.isSuccessCode(status)) {
            this._callback(this._data, this._channel);
        } else {
            this._error(this._channel);
        }
    },

    onChannelRedirect: function(oldChannel, newChannel, flags) {
        this._channel = newChannel;
    },

    getInterface: function(aIID) {
        try {
            return this.QueryInterface(aIID);
        } catch (e) {
            throw Components.results.NS_NOINTERFACE;
        }
    },

    QueryInterface: function(aIID) {
        if (aIID.equals(Components.interfaces.nsISupports) ||
            aIID.equals(Components.interfaces.nsIInterfaceRequestor) ||
            aIID.equals(Components.interfaces.nsIChannelEventSink) || 
            aIID.equals(Components.interfaces.nsIProgressEventSink) ||
            aIID.equals(Components.interfaces.nsIHttpEventSink) ||
            aIID.equals(Components.interfaces.nsIWebProgress) ||
            aIID.equals(Components.interfaces.nsIStreamListener))
        return this;

        throw Components.results.NS_NOINTERFACE;
    },

    // required but unnecessary and not implemented
    onProgress: function(req, context, progress, progressMax) { },
    onStatus: function(req, context, status, statusArg) { },
    onRedirect: function(oldChannel, newChannel) { },
    addProgressListener: function(listener, notifyMask) { },
    removeProgressListener: function(listener) { }
};
